﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using OpenTK.Graphics.OpenGL;
using OpenTK;
using System.Drawing;
using System.IO;
using System.Linq;
using SOM.Control;


namespace SOM.Model
{
    internal class Triangle
    {
        public List<MdNeuron> vertices;
        public Triangle(MdNeuron v1, MdNeuron v2, MdNeuron v3)
        {
            this.vertices = new List<MdNeuron>();
            vertices.Add(v1);
            vertices.Add(v2);
            vertices.Add(v3);
        }
    }

    public class SOMNetwork
    {
        private List<MdNeuron> NeuronList;


        private Queue<Triangle> Faces;
        private double leaarnFactor;
        private int iteration;
        private CompositionalSpectrum CSpectr;
        private double QE = 0;

        public double QuantizationError
        {
            get { return QE; }
            set { QE = value; }
        }
        public MainWindowCtrl main_control;

        public CompositionalSpectrum CompositionalSpectrum
        {
            get { return CSpectr; }
            set { CSpectr = value; }
        }

        internal List<MdNeuron> NetworkNeuronList
        {
            get { return NeuronList; }
            set { NeuronList = value; }
        }


        private int tessalationLevel;
        private int Epochs;
        private double KernelWidth;



        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="spectrum">Spectrum used to traain the network</param>
        /// <param name="tessalation"> Subdivisiion level , deteermines totoal network size</param>
        /// <param name="numEpochs"> Number of training cycles</param>
        /// <param name="initLearnRate"> Starting leaarning cooeficent </param>
        /// <param name="kernelWidth"></param>
        /// <param name="mw_control"></param>
        public SOMNetwork(CompositionalSpectrum spectrum, int tessalation, int numEpochs, double initLearnRate, double kernelWidth,MainWindowCtrl mw_control)
        {
            this.CSpectr = spectrum;
            this.tessalationLevel = tessalation;
            this.Faces = new Queue<Triangle>();
            this.NeuronList = new List<MdNeuron>();
            this.Epochs = numEpochs;
            this.leaarnFactor = initLearnRate;
            this.KernelWidth = kernelWidth;
            this.iteration = 0;
            this.main_control = mw_control;
        }

        private int findBMU(CSVector inputCSvector)// index of bmu from NeuronList relatively to the inputCSvector
        {
            double minDiff;
            int index = 0;
            int bmuIndex = 0;
            minDiff = inputCSvector.Coorelation(this.NeuronList[index].getCSVector());
            for (index = 1; index < this.NeuronList.Count; index++)
            {
                if (minDiff > inputCSvector.Coorelation(this.NeuronList[index].getCSVector()))
                {
                    bmuIndex = index;
                    minDiff = inputCSvector.Coorelation(this.NeuronList[index].getCSVector());
                }
            }
            return bmuIndex;
        }

        internal void init()
        {
            var t = (1.0 + Math.Sqrt(5.0)) / 2.0;

            MdNeuron p0 = new MdNeuron(new Vector3d(-1, t, 0));
            MdNeuron p1 = new MdNeuron(new Vector3d(1, t, 0));
            MdNeuron p2 = new MdNeuron(new Vector3d(-1, -t, 0));
            MdNeuron p3 = new MdNeuron(new Vector3d(1, -t, 0));

            MdNeuron p4 = new MdNeuron(new Vector3d(0, -1, t));
            MdNeuron p5 = new MdNeuron(new Vector3d(0, 1, t));
            MdNeuron p6 = new MdNeuron(new Vector3d(0, -1, -t));
            MdNeuron p7 = new MdNeuron(new Vector3d(0, 1, -t));

            MdNeuron p8 = new MdNeuron(new Vector3d(t, 0, -1));
            MdNeuron p9 = new MdNeuron(new Vector3d(t, 0, 1));
            MdNeuron p10 = new MdNeuron(new Vector3d(-t, 0, -1));
            MdNeuron p11 = new MdNeuron(new Vector3d(-t, 0, 1));

            //p0.initNeighboorlist(p1,p5,p7,p10,p11);
            //p1.initNeighboorlist(p7,p0,p8,p9,p5);
            //p2.initNeighboorlist(p6,p3,p4,p11,p10);
            //p3.initNeighboorlist(p4,p9,p8,p6,p2);
            //p4.initNeighboorlist(p2,p3,p5,p9,p11);
            //p5.initNeighboorlist(p1,p0,p4,p11,p9);
            //p6.initNeighboorlist(p2,p3,p7,p8,p10);
            //p7.initNeighboorlist(p1,p0,p6,p8,p10);
            //p8.initNeighboorlist(p1,p3,p6,p7,p9);
            //p9.initNeighboorlist(p1,p3,p4,p5,p8);
            //p10.initNeighboorlist(p0,p2,p6,p7,p11);
            //p11.initNeighboorlist(p0,p2,p4,p5,p10);

            Faces.Enqueue(new Triangle(p0, p11, p5));
            Faces.Enqueue(new Triangle(p0, p5, p1));
            Faces.Enqueue(new Triangle(p0, p1, p7));
            Faces.Enqueue(new Triangle(p0, p7, p10));
            Faces.Enqueue(new Triangle(p0, p10, p11));

            Faces.Enqueue(new Triangle(p1, p5, p9));
            Faces.Enqueue(new Triangle(p5, p11, p4));
            Faces.Enqueue(new Triangle(p11, p10, p2));
            Faces.Enqueue(new Triangle(p10, p7, p6));
            Faces.Enqueue(new Triangle(p7, p1, p8));

            Faces.Enqueue(new Triangle(p3, p9, p4));
            Faces.Enqueue(new Triangle(p3, p4, p2));
            Faces.Enqueue(new Triangle(p3, p2, p6));
            Faces.Enqueue(new Triangle(p3, p6, p8));
            Faces.Enqueue(new Triangle(p3, p8, p9));

            Faces.Enqueue(new Triangle(p4, p9, p5));
            Faces.Enqueue(new Triangle(p2, p4, p11));
            Faces.Enqueue(new Triangle(p6, p2, p10));
            Faces.Enqueue(new Triangle(p8, p6, p7));
            Faces.Enqueue(new Triangle(p9, p8, p1));

            subdivide(tessalationLevel);
            createNeighborList();

            foreach (var item in Faces)
            {
                if (!NeuronList.Contains(item.vertices[0])) NeuronList.Add(item.vertices[0]);
                if (!NeuronList.Contains(item.vertices[1])) NeuronList.Add(item.vertices[1]);
                if (!NeuronList.Contains(item.vertices[2])) NeuronList.Add(item.vertices[2]);
            }

            initNeuronWeghtVectors();
        }

        private void initNeuronWeghtVectors()
        {
            foreach (var item in NeuronList)
            {
                item.randInitFeatureVector(CSpectr.CSVectorList[0].getVectorSize(), (int)CSpectr.CSVectorList[0].getMax());
            }
        }

        internal void trainSOM()
        {
            double Lfactor_zero = leaarnFactor;
            double kWidth_zero = KernelWidth;

            for (int i = 0; i < Epochs; i++)
            {
                iteration++;
                KernelWidth = kWidth_zero * (this.Epochs - this.iteration) / (double)this.Epochs;
                leaarnFactor = Lfactor_zero * (this.Epochs - this.iteration) / (double)this.Epochs;
                foreach (var input_pattern in CSpectr.CSVectorList)
                {
                    int BMUindex = findBMU(input_pattern);
                    updateSOMNetwork(BMUindex, input_pattern);
                }

                main_control.SetProgressBarValue((int)(50+((i/(double)Epochs*100)/2)));
                
            }
            QE = calcQuantizationError();
        }

        private void updateSOMNetwork(int BMUindex, CSVector input_pattern)
        {
            List<MdNeuron> parent = new List<MdNeuron>();
            List<MdNeuron> childrens = new List<MdNeuron>();
            MdNeuron bmuNeuron = NeuronList[BMUindex];
            parent.Add(bmuNeuron);
            parent[0].getCSVector().updateWeight(leaarnFactor, input_pattern);
            parent[0].Visited = true;
            int depth = 0;

            while (true)
            {
                while (parent.Count != 0)
                {
                    foreach (var item in parent[0].getDirectNeighborsList())
                    {
                        if (!item.Visited)
                        {
                            item.Visited = true;
                            childrens.Add(item);
                        }
                    }
                    parent.RemoveAt(0);
                }
                if (childrens.Count == 0)
                {
                    break;
                }
                depth++;
                parent.AddRange(childrens);
                updateParentNodes(parent, depth, input_pattern);
                childrens.Clear();
            }

            for (int i = 0; i < NeuronList.Count; i++) NeuronList[i].Visited = false;
        }

        private void updateParentNodes(List<MdNeuron> parent, int depth, CSVector input_pattern)
        {
            double exp = Math.Exp(-Math.Pow(depth, 2) / (2 * Math.Pow(KernelWidth, 2)));
            //double exp = Math.Exp(-depth / ( Math.Pow(KernelWidth, 1.5)));
            double theta = leaarnFactor * exp;

            //if (depth > KernelWidth) theta = 0;
            //else theta = leaarnFactor;


            for (int i = 0; i < parent.Count; i++)
            {
                parent[i].getCSVector().updateWeight(theta, input_pattern);
            }
        }

        private MdNeuron getMiddleVertex(MdNeuron v1, MdNeuron v2)
        {
            double x = (v1.getNeuronCoordinates().X + v2.getNeuronCoordinates().X) / 2.0;
            double y = (v1.getNeuronCoordinates().Y + v2.getNeuronCoordinates().Y) / 2.0;
            double z = (v1.getNeuronCoordinates().Z + v2.getNeuronCoordinates().Z) / 2.0;

            Vector3d new_neuronCoord = new Vector3d(x, y, z);
            MdNeuron new_vertex = new MdNeuron(new_neuronCoord);
            return new_vertex;
        }

        private void createNeighborList()
        {
            foreach (var item in Faces)
            {
                foreach (var vertex in item.vertices)
                {
                    List<Triangle> participateFaces = participateFacesList(vertex);
                    foreach (var triangle in participateFaces)
                    {
                        if (!triangle.vertices[0].getNeuronCoordinates().Equals(vertex.getNeuronCoordinates()) && !vertex.getDirectNeighborsList().Contains(triangle.vertices[0]))
                            vertex.AddNeighboor(triangle.vertices[0]);
                        if (!triangle.vertices[1].getNeuronCoordinates().Equals(vertex.getNeuronCoordinates()) && !vertex.getDirectNeighborsList().Contains(triangle.vertices[1]))
                            vertex.AddNeighboor(triangle.vertices[1]);
                        if (!triangle.vertices[2].getNeuronCoordinates().Equals(vertex.getNeuronCoordinates()) && !vertex.getDirectNeighborsList().Contains(triangle.vertices[2]))
                            vertex.AddNeighboor(triangle.vertices[2]);
                    }
                }
            }
        }

        private List<Triangle> participateFacesList(MdNeuron m)
        {
            List<Triangle> participateFaces = new List<Triangle>();
            foreach (var triangle in Faces)
            {
                if (triangle.vertices[0].getNeuronCoordinates().Equals(m.getNeuronCoordinates())) { participateFaces.Add(triangle); continue; }
                if (triangle.vertices[1].getNeuronCoordinates().Equals(m.getNeuronCoordinates())) { participateFaces.Add(triangle); continue; }
                if (triangle.vertices[2].getNeuronCoordinates().Equals(m.getNeuronCoordinates())) { participateFaces.Add(triangle); continue; }
            }
            return participateFaces;
        }

        private void subdivide(int subdiv_level)
        {
            Queue<Triangle> faces_store = new Queue<Triangle>();
            int Faces_size = Faces.Count;

            for (int i = 0; i < subdiv_level - 1; i++) // subdiv passes
            {

                for (int j = 0; j < Faces_size; j++)
                {
                    Triangle current = Faces.Dequeue();
                    MdNeuron v01 = getMiddleVertex(current.vertices[0], current.vertices[1]);
                    MdNeuron v02 = getMiddleVertex(current.vertices[0], current.vertices[2]);
                    MdNeuron v12 = getMiddleVertex(current.vertices[1], current.vertices[2]);

                    //TODO - we create multiplicates here ///Check that this is correct handling of duplicates
                    foreach (var face in faces_store)
                    {
                        if (face.vertices[0].getNeuronCoordinates().Equals(v01.getNeuronCoordinates())) v01 = face.vertices[0];
                        if (face.vertices[1].getNeuronCoordinates().Equals(v01.getNeuronCoordinates())) v01 = face.vertices[1];
                        if (face.vertices[2].getNeuronCoordinates().Equals(v01.getNeuronCoordinates())) v01 = face.vertices[2];

                        if (face.vertices[0].getNeuronCoordinates().Equals(v02.getNeuronCoordinates())) v02 = face.vertices[0];
                        if (face.vertices[1].getNeuronCoordinates().Equals(v02.getNeuronCoordinates())) v02 = face.vertices[1];
                        if (face.vertices[2].getNeuronCoordinates().Equals(v02.getNeuronCoordinates())) v02 = face.vertices[2];

                        if (face.vertices[0].getNeuronCoordinates().Equals(v12.getNeuronCoordinates())) v12 = face.vertices[0];
                        if (face.vertices[1].getNeuronCoordinates().Equals(v12.getNeuronCoordinates())) v12 = face.vertices[1];
                        if (face.vertices[2].getNeuronCoordinates().Equals(v12.getNeuronCoordinates())) v12 = face.vertices[2];

                    }

                    faces_store.Enqueue(new Triangle(v01, v02, v12));
                    faces_store.Enqueue(new Triangle(current.vertices[0], v01, v02));
                    faces_store.Enqueue(new Triangle(current.vertices[1], v01, v12));
                    faces_store.Enqueue(new Triangle(current.vertices[2], v02, v12));
                }
                //clear the Faces queue
                //move all from faces_store to faces
                Faces.Clear();
                foreach (var item in faces_store)
                {
                    Faces.Enqueue(item);
                }
                faces_store.Clear();
                Faces_size = Faces.Count;
            }
        }

        private double calcQuantizationError()
        {
            double qe = 0;                                 //QuantizationError
            int count = CSpectr.CSVectorList.Count;
            foreach (var item in CSpectr.CSVectorList)
            {
                qe += item.vecSubtraction(NeuronList[findBMU(item)].getCSVector(), item).getCsVectorSizeNormal();
            }
            qe /= count;
            return qe;
        }

        public void calculateInputPatternsColors(int index)
        {
            int r;
            int g;
            int b;
            List<double> diff = new List<double>();
            CSVector basic = new CSVector(CSpectr.CSVectorList[0].getVectorSize());
            basic.fillIncreasingValues();

            for (int i = 0; i < this.CSpectr.CSVectorList.Count; i++)
            {
                diff.Add(basic.Coorelation(this.CSpectr.CSVectorList[i]));
            }
            double max = diff.Max();
            double min = diff.Min();

            for (int j = 0; j < diff.Count; j++)
            {
                double hsvColorValue = (280 / (max - min)) * (double)(diff[j] - min);
                CSpectr.CSVectorList[j].HSV = hsvColorValue;
                HsvToRgb(hsvColorValue, 1.0, 1.0, out r, out g, out b);
                CSpectr.CSVectorList[j].R = r;
                CSpectr.CSVectorList[j].G = g;
                CSpectr.CSVectorList[j].B = b;
            }
        }

        //DRAWING

        void HsvToRgb(double h, double S, double V, out int r, out int g, out int b)
        {

            // C/C++ Macro HSV to RGB

            double H = h;
            while (H < 0) { H += 360; };
            while (H >= 360) { H -= 360; };
            double R, G, B;
            if (V <= 0)
            { R = G = B = 0; }
            else if (S <= 0)
            {
                R = G = B = V;
            }
            else
            {
                double hf = H / 60.0;
                int i = (int)Math.Floor(hf);
                double f = hf - i;
                double pv = V * (1 - S);
                double qv = V * (1 - S * f);
                double tv = V * (1 - S * (1 - f));
                switch (i)
                {

                    // Red is the dominant color

                    case 0:
                        R = V;
                        G = tv;
                        B = pv;
                        break;

                    // Green is the dominant color

                    case 1:
                        R = qv;
                        G = V;
                        B = pv;
                        break;
                    case 2:
                        R = pv;
                        G = V;
                        B = tv;
                        break;

                    // Blue is the dominant color

                    case 3:
                        R = pv;
                        G = qv;
                        B = V;
                        break;
                    case 4:
                        R = tv;
                        G = pv;
                        B = V;
                        break;

                    // Red is the dominant color

                    case 5:
                        R = V;
                        G = pv;
                        B = qv;
                        break;

                    // Just in case we overshoot on our math by a little, we put these here. Since its a switch it won't slow us down at all to put these here.

                    case 6:
                        R = V;
                        G = tv;
                        B = pv;
                        break;
                    case -1:
                        R = V;
                        G = pv;
                        B = qv;
                        break;

                    // The color is not defined, we should throw an error.

                    default:
                        //LFATAL("i Value error in Pixel conversion, Value is %d", i);
                        R = G = B = V; // Just pretend its black/white
                        break;
                }
            }
            r = Clamp((int)(R * 255.0));
            g = Clamp((int)(G * 255.0));
            b = Clamp((int)(B * 255.0));
        }

        /// <summary>
        /// Clamp a value to 0-255
        /// </summary>
        int Clamp(int i)
        {
            if (i < 0) return 0;
            if (i > 255) return 255;
            return i;
        }

        public void computeneuroncolors()
        {
            int r;
            int g;
            int b;
            foreach (var item in NeuronList)
            {
                item.getCSVector().HSV = 0;
                double alpha_sum = 0;
                foreach (var pattren in CSpectr.CSVectorList)
                {
                    double alpha = 1 / (double)pattren.Coorelation(item.getCSVector());
                    alpha_sum += alpha;
                    item.getCSVector().HSV += pattren.HSV * alpha;

                }
                item.getCSVector().HSV /= alpha_sum;
                HsvToRgb(item.getCSVector().HSV, 1.0, 1.0, out r, out g, out b);
                item.getCSVector().R = r;
                item.getCSVector().G = g;
                item.getCSVector().B = b;
            }
        }

        public void VisualizeSOM(Color color)
        {
            int r;
            int g;
            int b;
            GL.Begin(BeginMode.Triangles);
            foreach (var face in Faces)
            {
                HsvToRgb(face.vertices[0].getCSVector().HSV, 1.0, 1.0, out r, out g, out b);
                face.vertices[0].getCSVector().R = r;
                face.vertices[0].getCSVector().G = g;
                face.vertices[0].getCSVector().B = b;


             
                GL.Color3(Color.FromArgb(r, g, b));
                //GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[0].getNeuronCoordinates().Normalized());

                HsvToRgb(face.vertices[1].getCSVector().HSV, 1.0, 1.0, out r, out g, out b);
                face.vertices[1].getCSVector().R = r;
                face.vertices[1].getCSVector().G = g;
                face.vertices[1].getCSVector().B = b;

                GL.Color3(Color.FromArgb(r, g, b));
                //GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[1].getNeuronCoordinates().Normalized());

                HsvToRgb(face.vertices[2].getCSVector().HSV, 1, 1, out r, out g, out b);
                face.vertices[2].getCSVector().R = r;
                face.vertices[2].getCSVector().G = g;
                face.vertices[2].getCSVector().B = b;

                GL.Color3(Color.FromArgb(r, g, b));
                //GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[2].getNeuronCoordinates().Normalized());
            }
            GL.End();
        }

        public void VisualizeSOM_old(Color color)
        {
            double max = 0;
            double cooef = 0;
            double min;
            int r;
            int g;
            int b;



            foreach (var item in NeuronList)
            {
                if (max < item.getCSVector().getCsVectorSizeNormal()) max = item.getCSVector().getCsVectorSizeNormal();
            }
            min = max;
            foreach (var item in NeuronList)
            {
                if (min > item.getCSVector().getCsVectorSizeNormal()) min = item.getCSVector().getCsVectorSizeNormal();
            }


            cooef = 360.0 / (max - min);

            GL.Begin(BeginMode.Triangles);
            foreach (var face in Faces)
            {


                HsvToRgb(face.vertices[0].getCSVector().getCsVectorSizeNormal() * cooef - (cooef * min), 1.0, 1.0, out r, out g, out b);
                GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[0].getNeuronCoordinates().Normalized());

                HsvToRgb(face.vertices[1].getCSVector().getCsVectorSizeNormal() * cooef - (cooef * min), 1.0, 1.0, out r, out g, out b);
                GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[1].getNeuronCoordinates().Normalized());

                HsvToRgb(face.vertices[2].getCSVector().getCsVectorSizeNormal() * cooef - (cooef * min), 1, 1, out r, out g, out b);
                GL.Color3((float)r, (float)g, (float)b);
                GL.Vertex3(face.vertices[2].getNeuronCoordinates().Normalized());

            }
            GL.End();
        }

        public void DrawPolyFrame(Color color)
        {
            GL.Begin(BeginMode.Triangles);
            foreach (var face in Faces)
            {
                GL.Color3(color);
                GL.Vertex3(face.vertices[0].getNeuronCoordinates().Normalized());

                GL.Color3(color);
                GL.Vertex3(face.vertices[1].getNeuronCoordinates().Normalized());

                GL.Color3(color);
                GL.Vertex3(face.vertices[2].getNeuronCoordinates().Normalized());
            }
            GL.End();
        }

        public void DrawGrid(System.Drawing.Color color, float X, float Z, int cell_size = 16, int grid_size = 256)
        {
            int dX = (int)Math.Round(X / cell_size) * cell_size;
            int dZ = (int)Math.Round(Z / cell_size) * cell_size;

            int ratio = grid_size / cell_size;

            GL.PushMatrix();

            GL.Translate(dX - grid_size / 2, 0, dZ - grid_size / 2);

            int i;

            GL.Color3(color);
            GL.Begin(BeginMode.Lines);

            for (i = 0; i < ratio + 1; i++)
            {
                int current = i * cell_size;

                GL.Vertex3(current, 0, 0);
                GL.Vertex3(current, 0, grid_size);

                GL.Vertex3(0, 0, current);
                GL.Vertex3(grid_size, 0, current);
            }

            GL.End();

            GL.PopMatrix();
        }
    }
}
